# -*- test-case-name: twisted.test.test_factories,twisted.internet.test.test_protocol -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

"""
Standard implementations of Twisted protocol-related interfaces.

Start here if you are looking to write a new protocol implementation for
Twisted.  The Protocol class contains some introductory material.
"""

__all__ = (
	'Protocol', 'Factory',
	'ClientFactory', 'ServerFactory', 'ReconnectingClientFactory',
	'ClientCreator',
	'connectionDone',
)

import random

import eof.failure as failure
import eof.log     as log
import eof.util    as U
import eof.ztk     as ztk

from . import defer
from . import errors
from . import ifaces


@ztk.implementer(ifaces.IProtocolFactory)  # ifaces.ILoggingContext
class Factory:
	"""This is a factory which produces protocols.

	By default, buildProtocol will create a protocol of the class given in
	self.protocol."""

	# Put a subclass of Protocol here:
	protocol = None

	numPorts = 0
	noisy    = True

	@classmethod
	def forProtocol(cls, protocol, *args, **kwargs):
		"""Create a factory for the given protocol.

		It sets the C{protocol} attribute and returns the constructed factory
		instance.

		@param protocol: A L{Protocol} subclass

		@param args: Positional arguments for the factory.

		@param kwargs: Keyword arguments for the factory.

		@return: A L{Factory} instance wired up to C{protocol}."""

		factory = cls(*args, **kwargs)
		factory.protocol = protocol

		return factory

#	def logPrefix(self):
#		return type(self).__name__

	def doStart(self):
		"""Make sure startFactory is called.

		Users should not call this function themselves!"""

		if not self.numPorts:
			if self.noisy:
				log.debug('Starting factory {factory!r}', factory=self)
			self.startFactory()
		self.numPorts = self.numPorts + 1

	def doStop(self):
		"""Make sure stopFactory is called.

		Users should not call this function themselves!"""

		if self.numPorts == 0:
			# This shouldn't happen, but does sometimes and this is better
			# than blowing up in assert as we did previously.
			return

		self.numPorts -= 1

		if not self.numPorts:
			if self.noisy:
				log.debug('Stopping factory {factory!r}', factory=self)
			self.stopFactory()

	def startFactory(self):
		"""This will be called before I begin listening on a Port or Connector.

		It will only be called once, even if the factory is connected
		to multiple ports.

		This can be used to perform 'unserialization' tasks that
		are best put off until things are actually running, such
		as connecting to a database, opening files, etcetera."""

	def stopFactory(self):
		"""This will be called before I stop listening on all Ports/Connectors.

		This can be overridden to perform 'shutdown' tasks such as disconnecting
		database connections, closing files, etc.

		It will be called, for example, before an application shuts down,
		if it was connected to a port. User code should not call this function
		directly."""

	def buildProtocol(self, addr):
		"""Create an instance of a subclass of Protocol.

		The returned instance will handle input on an incoming server
		connection, and an attribute "factory" pointing to the creating
		factory.

		Alternatively, L{None} may be returned to immediately close the
		new connection.

		Override this method to alter how Protocol instances get created.

		@param addr: an object implementing L{ifaces.IAddress}"""

		p = self.protocol()
		p.factory = self
		return p


class ClientFactory(Factory):
    """
    A Protocol factory for clients.

    This can be used together with the various connectXXX methods in
    reactors.
    """

    def startedConnecting(self, connector):
        """
        Called when a connection has been started.

        You can call connector.stopConnecting() to stop the connection attempt.

        @param connector: a Connector object.
        """


    def clientConnectionFailed(self, connector, reason):
        """
        Called when a connection has failed to connect.

        It may be useful to call connector.connect() - this will reconnect.

        @type reason: L{twisted.python.failure.Failure}
        """


    def clientConnectionLost(self, connector, reason):
        """
        Called when an established connection is lost.

        It may be useful to call connector.connect() - this will reconnect.

        @type reason: L{twisted.python.failure.Failure}
        """


class _InstanceFactory(ClientFactory):
    """
    Factory used by ClientCreator.

    @ivar deferred: The L{Deferred} which represents this connection attempt and
        which will be fired when it succeeds or fails.

    @ivar pending: After a connection attempt succeeds or fails, a delayed call
        which will fire the L{Deferred} representing this connection attempt.
    """

    noisy = False
    pending = None

    def __init__(self, reactor, instance, deferred):
        self.reactor  = reactor
        self.instance = instance
        self.deferred = deferred

    def __repr__(self):
        return "<ClientCreator factory: %r>" % (self.instance, )

    def buildProtocol(self, addr):
        """
        Return the pre-constructed protocol instance and arrange to fire the
        waiting L{Deferred} to indicate success establishing the connection.
        """
        self.pending = self.reactor.callLater(0, self.fire, self.deferred.callback, self.instance)
        self.deferred = None
        return self.instance

    def clientConnectionFailed(self, connector, reason):
        """
        Arrange to fire the waiting L{Deferred} with the given failure to
        indicate the connection could not be established.
        """
        self.pending = self.reactor.callLater(0, self.fire, self.deferred.errback, reason)
        self.deferred = None

    def fire(self, func, value):
        """Clear C{self.pending} to avoid a reference cycle and then invoke func
        with the value."""

        self.pending = None
        func(value)


class ClientCreator:
	"""Client connections that do not require a factory.

	The various connect* methods create a protocol instance using the given
	protocol class and arguments, and connect it, returning a Deferred of the
	resulting protocol instance.

	Useful for cases when we don't really need a factory.  Mainly this
	is when there is no shared state between protocol instances, and no need
	to reconnect.

	The C{connectTCP}, C{connectUNIX}, and C{connectSSL} methods each return a
	L{Deferred} which will fire with an instance of the protocol class passed to
	L{ClientCreator.__init__}.  These Deferred can be cancelled to abort the
	connection attempt (in a very unlikely case, cancelling the Deferred may not
	prevent the protocol from being instantiated and connected to a transport;
	if this happens, it will be disconnected immediately afterwards and the
	Deferred will still errback with L{CancelledError})."""

	def __init__(self, reactor, protocolClass, *args, **kwargs):
		self.reactor = reactor
		self.protocolClass = protocolClass
		self.args = args
		self.kwargs = kwargs

	def _connect(self, method, *args, **kwargs):
		"""Initiate a connection attempt.

	@param method: A callable which will actually start the connection
		attempt.  For example, C{reactor.connectTCP}.

	@param *args: Positional arguments to pass to C{method}, excluding the
		factory.

	@param **kwargs: Keyword arguments to pass to C{method}.

	@return: A L{Deferred} which fires with an instance of the protocol
		class passed to this L{ClientCreator}'s initializer or fails if the
		connection cannot be set up for some reason."""

		def cancelConnect(deferred):
			connector.disconnect()
			if f.pending is not None:
				f.pending.cancel()

		d = defer.Deferred(cancelConnect)
		f = _InstanceFactory(self.reactor, self.protocolClass(*self.args, **self.kwargs), d)
		connector = method(factory=f, *args, **kwargs)

		return d

	def connectTCP(self, host, port, timeout=30, bindAddress=None):
		"""
		Connect to a TCP server.

		The parameters are all the same as to L{IReactorTCP.connectTCP} except
		that the factory parameter is omitted.

		@return: A L{Deferred} which fires with an instance of the protocol
			class passed to this L{ClientCreator}'s initializer or fails if the
			connection cannot be set up for some reason.
		"""
		return self._connect(
			self.reactor.connectTCP, host, port, timeout=timeout,
			bindAddress=bindAddress)

	def connectUNIX(self, address, timeout=30, checkPID=False):
		"""Connect to a Unix socket.

	The parameters are all the same as to L{IReactorUNIX.connectUNIX} except
	that the factory parameter is omitted.

	@return: A L{Deferred} which fires with an instance of the protocol
		class passed to this L{ClientCreator}'s initializer or fails if the
		connection cannot be set up for some reason."""

		return self._connect(
			self.reactor.connectUNIX, address,
			timeout =timeout,
			checkPID=checkPID)

	def connectSSL(self, host, port, contextFactory, timeout=30, bindAddress=None):
		"""Connect to an SSL server.

	The parameters are all the same as to L{IReactorSSL.connectSSL} except
	that the factory parameter is omitted.

	@return: A L{Deferred} which fires with an instance of the protocol
		class passed to this L{ClientCreator}'s initializer or fails if the
		connection cannot be set up for some reason."""

		return self._connect(
			self.reactor.connectSSL, host, port,
			contextFactory=contextFactory,
			timeout       =timeout,
			bindAddress   =bindAddress)


class ReconnectingClientFactory(ClientFactory):
    """
    Factory which auto-reconnects clients with an exponential back-off.

    Note that clients should call my resetDelay method after they have
    connected successfully.

    @ivar maxDelay: Maximum number of seconds between connection attempts.
    @ivar initialDelay: Delay for the first reconnection attempt.
    @ivar factor: A multiplicitive factor by which the delay grows
    @ivar jitter: Percentage of randomness to introduce into the delay length
        to prevent stampeding.
    @ivar clock: The clock used to schedule reconnection. It's mainly useful to
        be parametrized in tests. If the factory is serialized, this attribute
        will not be serialized, and the default value (the reactor) will be
        restored when deserialized.
    @type clock: L{IReactorTime}
    @ivar maxRetries: Maximum number of consecutive unsuccessful connection
        attempts, after which no further connection attempts will be made. If
        this is not explicitly set, no maximum is applied.
    """

    maxDelay = 3600
    initialDelay = 1.0

    # Note: These highly sensitive factors have been precisely measured by
    # the National Institute of Science and Technology.  Take extreme care
    # in altering them, or you may damage your Internet!
    # (Seriously: <http://physics.nist.gov/cuu/Constants/index.html>)
    factor = 2.7182818284590451 # (math.e)
    # Phi = 1.6180339887498948 # (Phi is acceptable for use as a
    # factor if e is too large for your application.)

    # This is the value of the molar Planck constant times c, joule
    # meter/mole.  The value is attributable to
    # https://physics.nist.gov/cgi-bin/cuu/Value?nahc|search_for=molar+planck+constant+times+c
    jitter = 0.119626565582

    delay = initialDelay
    retries = 0
    maxRetries = None
    _callID = None
    connector = None
    clock = None

    continueTrying = 1

    def clientConnectionFailed(self, connector, reason):
        if self.continueTrying:
            self.connector = connector
            self.retry()

    def clientConnectionLost(self, connector, unused_reason):
        if self.continueTrying:
            self.connector = connector
            self.retry()

    def retry(self, connector=None):
        """
        Have this connector connect again, after a suitable delay.
        """
        if not self.continueTrying:
            if self.noisy:
                log.debug('Abandoning {connector!r} on explicit request', connector=connector)
            return

        if connector is None:
            if self.connector is None:
                raise ValueError("no connector to retry")
            else:
                connector = self.connector

        self.retries += 1
        if self.maxRetries is not None and (self.retries > self.maxRetries):
            if self.noisy:
                log.debug('Abandoning {connector!r} after {retries} retries', connector=connector, retries=self.retries)
            return

        self.delay = min(self.delay * self.factor, self.maxDelay)
        if self.jitter:
            self.delay = random.normalvariate(self.delay,
                                              self.delay * self.jitter)

        if self.noisy:
            log.debug('{connector!r} will retry in {delay} seconds', connector=connector, delay=self.delay)

        def reconnector():
            self._callID = None
            connector.connect()
        if self.clock is None:
            from twisted.internet import reactor
            self.clock = reactor
        self._callID = self.clock.callLater(self.delay, reconnector)

    def stopTrying(self):
        """
        Put a stop to any attempt to reconnect in progress.
        """
        # ??? Is this function really stopFactory?
        if self._callID:
            self._callID.cancel()
            self._callID = None
        self.continueTrying = 0
        if self.connector:
            try:
                self.connector.stopConnecting()
            except errors.NotConnectingError:
                pass

    def resetDelay(self):
        """
        Call this method after a successful connection: it resets the delay and
        the retry counter.
        """
        self.delay = self.initialDelay
        self.retries = 0
        self._callID = None
        self.continueTrying = 1

    def __getstate__(self):
        """
        Remove all of the state which is mutated by connection attempts and
        failures, returning just the state which describes how reconnections
        should be attempted.  This will make the unserialized instance
        behave just as this one did when it was first instantiated.
        """
        state = self.__dict__.copy()
        for key in ['connector', 'retries', 'delay',
                    'continueTrying', '_callID', 'clock']:
            if key in state:
                del state[key]
        return state


class ServerFactory(Factory):
	"""Subclass this to indicate that your protocol.Factory is only usable for servers."""

	def __repr__(self):
		return '<{}>'.format(type(self).__name__)


class BaseProtocol:
	"""This is the abstract superclass of all protocols.

	Some methods have helpful default implementations here so that they can
	easily be shared, but otherwise the direct subclasses of this class are more
	interesting, L{Protocol} and L{ProcessProtocol}."""

	connected = False
	transport = None

	def makeConnection(self, transport):
		"""Make a connection to a transport and a server.

		This sets the 'transport' attribute of this Protocol, and calls the
		connectionMade() callback."""

		self.connected = True
		self.transport = transport

		try:
			self.connectionMade()
		except Exception:
			# @xxx: это должен делать Twisted, иначе зависнет
			log.failure('Failure during `connectionMade` of {protocol}', protocol=self)
			transport.loseConnection(failure.Failure())  # @xxx: (ConnectionMixin, etc.).loseConnection has no reason
		except:
			transport.loseConnection(failure.Failure())
			raise

	def connectionMade(self):
		"""Called when a connection is made.

		This may be considered the initializer of the protocol, because
		it is called when the connection is completed.  For clients,
		this is called once the connection to the server has been
		established; for servers, this is called after an accept() call
		stops blocking and a socket has been received.  If you need to
		send any greeting or initial message, do it here."""


connectionDone = failure.Failure(errors.ConnectionDone())
connectionDone.cleanFailure()


@ztk.implementer(ifaces.IProtocol)  # ifaces.ILoggingContext
class Protocol(BaseProtocol):
	"""This is the base class for streaming connection-oriented protocols.

	If you are going to write a new connection-oriented protocol for Twisted,
	start here.  Any protocol implementation, either client or server, should
	be a subclass of this class.

	The API is quite simple.  Implement L{dataReceived} to handle both
	event-based and synchronous input; output can be sent through the
	'transport' attribute, which is to be an instance that implements
	L{.ifaces.ITransport}.  Override C{connectionLost} to be
	notified when the connection ends.

	Some subclasses exist already to help you write common types of protocols:
	see the L{twisted.protocols.basic} module for a few of them."""

#	def logPrefix(self):
#		return type(self).__name__

	def dataReceived(self, data):
		"""Called whenever data is received.

		Use this method to translate to a higher-level message.  Usually, some
		callback will be made upon the receipt of each complete protocol
		message.

		@param data: a string of indeterminate length.  Please keep in mind
			that you will probably need to buffer some data, as partial
			(or multiple) protocol messages may be received!  I recommend
			that unit tests for protocols call through to this method with
			differing chunk sizes, down to one byte at a time."""

	def connectionLost(self, reason=connectionDone):
		"""Called when the connection is shut down.

		Clear any circular references here, and any external references
		to this Protocol.  The connection has been closed.

		@type reason: L{twisted.python.failure.Failure}"""


#class ProtocolToConsumerAdapter(components.Adapter):
#class ConsumerToProtocolAdapter(components.Adapter):
#class DatagramProtocol(AbstractDatagramProtocol):
#class ConnectedDatagramProtocol(DatagramProtocol):
#class FileWrapper:
#...
